﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.CSharp;
using OperationPlugins.Compositions;
using OperationPlugins.ExtensionMethods;
using OperationPlugins.Models;

namespace OperationPlugins.Proxies.CodeDom
{
    [Export(typeof(ITypeValidator))]
    [Export(typeof(IProxyActivator))]
    [Export(typeof(IProxyGenerator))]
    [DefaultPriority]
    internal sealed class CodeDomProxyGenerator : ITypeValidator, IProxyActivator, IProxyGenerator
    {
        /// <summary>
        /// The interface type.
        /// </summary>
        private Type Interface { get; set; }

        /// <summary>
        /// All types.
        /// </summary>
        private List<Type> InterfaceTypes { get; set; }

        /// <summary>
        /// All properties.
        /// </summary>
        private Dictionary<Type, List<PropertyInfo>> InterfaceProperties { get; set; }

        /// <summary>
        /// All methods.
        /// </summary>
        private Dictionary<Type, List<MethodInfo>> InterfaceMethods { get; set; }

        /// <summary>
        /// All events.
        /// </summary>
        private Dictionary<Type, List<EventInfo>> InterfaceEvents { get; set; }

        /// <summary>
        /// The code compilation unit.
        /// </summary>
        private CodeCompileUnit CompilationUnit { get; set; }

        /// <summary>
        /// All referenced assemblies.
        /// </summary>
        private AssemblyCollection ReferencedAssemblies { get; set; }

        /// <summary>
        /// The proxy type namespace.
        /// </summary>
        private string ProxyTypeNamespace { get; set; }

        /// <summary>
        /// The proxy type name.
        /// </summary>
        private string ProxyTypeName { get; set; }

        /// <summary>
        /// Code generators.
        /// </summary>
        private static IEnumerable<IExternalCodeGenerator> CodeGenerators { set; get; }

        /// <summary>
        /// Proxy assembly notification listeners.
        /// </summary>
        private static IEnumerable<IProxyAssemblyNotificationListener> ProxyAssemblyNotificationListeners { set; get; }

        /// <summary>
        /// Attribute argument value parsers.
        /// </summary>
        private static IEnumerable<ICustomAttributeTypedArgumentValueParser> Parsers { set; get; }

        /// <summary>
        /// Checks if a proxy can be generated from the specified type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="message">If this method returns false, contains an error message that describes the reason why the specified type cannot be used.</param>
        /// <returns>true if a proxy can be generated from the specified type, otherwise false.</returns>
        bool ITypeValidator.IsValid(Type type, out string message)
        {
            message = null;

            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            if ((type.IsInterface && type.IsPublic && type.IsGenericType == false) == false)
            {
                message = "The type must be a public non-generic interface.";
                return false;
            }

            return true;
        }

        /// <summary>
        /// Generates a new proxy type.
        /// </summary>
        /// <param name="type">The type</param>
        /// <returns>The proxy type</returns>
        Type IProxyGenerator.Generate(Type type)
        {
            Interface = type;
            ResolveInterfaceMembers();
            CompilationUnit = new CodeCompileUnit();
            ReferencedAssemblies = new AssemblyCollection();

            CreateCompilationUnit();

            using (var provider = new CSharpCodeProvider())
            {
                // Resolve referenced assemblies.
                string[] locations = ReferencedAssemblies.GetLocations();

                // Compiler parameters.
                CompilerParameters parameters = new CompilerParameters();
                parameters.ReferencedAssemblies.AddRange(locations);
                parameters.TreatWarningsAsErrors = false;

#if (DEBUG)
                parameters.GenerateInMemory = false;
                parameters.TempFiles = new TempFileCollection(Environment.GetEnvironmentVariable("TEMP"), true);
                parameters.IncludeDebugInformation = true;
                parameters.TempFiles.KeepFiles = true;
#else
                parameters.GenerateInMemory = true;
                parameters.IncludeDebugInformation = false;
#endif
                // Compile
                CompilerResults cr = provider.CompileAssemblyFromDom(parameters, CompilationUnit);

                // If the compile failed, then throw an exception with source code.
                if (cr.Errors.Count > 0)
                {
                    string src;
                    using (var stream = new MemoryStream())
                    {
                        using (var writer = new StreamWriter(stream))
                        {
                            provider.GenerateCodeFromCompileUnit(CompilationUnit, writer, new CodeGeneratorOptions { BracingStyle = "C" });
                            stream.Position = 0;
                            src = new StreamReader(stream).ReadToEnd();
                        }
                    }

                    throw new ProxyCompileException(cr.Errors, src);
                }

                // Find the compiled proxy assembly and notify listeners.
                Assembly assembly = cr.CompiledAssembly;
                foreach (IProxyAssemblyNotificationListener l in ProxyAssemblyNotificationListeners)
                {
                    l.OnAssemblyCompiled(assembly);
                }

                // Find the proxy type from the compiled assembly.
                string proxyTypeFullName = string.Format("{0}.{1}", ProxyTypeNamespace, ProxyTypeName);
                Type proxyType = assembly.GetType(proxyTypeFullName, true, false);

                return proxyType;
            }
        }

        /// <summary>
        /// Activates a proxy.
        /// </summary>
        /// <typeparam name="T">The type</typeparam>
        /// <param name="proxyType">The proxy type</param>
        /// <returns>The proxy</returns>
        T IProxyActivator.Activate<T>(Type proxyType)
        {
            return (T)ObjectFactory.CreateClassInstance(proxyType);
        }

        private void CreateCompilationUnit()
        {
            CreateNamespace();
            CreateClass();
            CreateProperties();
            CreateEvents();
            CreateMethods();
        }

        #region Namespace

        /// <summary>
        /// Creates a namespace.
        /// </summary>
        private void CreateNamespace()
        {
            string name = "OperationPlugins.Proxies." + Interface.GetFullName().GetHashCode().ToString(CultureInfo.CurrentCulture).Replace('-', '_');

            ProxyTypeNamespace = name;

            CompilationUnit.Namespaces.Add(new CodeNamespace(name));            
        }

        #endregion

        #region Class

        /// <summary>
        /// Creates a proxy class.
        /// </summary>
        private void CreateClass()
        {
            var code = new CodeTypeDeclaration();

            string name = string.Format("ProxyFor{0}", Interface.Name);
            ProxyTypeName = name;

            code.Name = name;
            code.IsClass = true;

            // Define the base type and inherited interfaces.
            code.BaseTypes.Add(typeof(ModelBase));
            code.BaseTypes.Add(Interface);

            // Resolve custom attributes.
            code.CustomAttributes.AddRange(CreateCustomAttributes(Interface));

            // Resolve referenced assemblies.
            ReferencedAssemblies.AddFromType(typeof(ModelBase));
            ReferencedAssemblies.AddFromType(Interface);

            // Add the class to the namespace.
            CompilationUnit.Namespaces[0].Types.Add(code);            
        }

        #endregion

        #region Properties

        /// <summary>
        /// Creates properties.
        /// </summary>
        private void CreateProperties()
        {
            foreach (Type type in InterfaceTypes)
            {
                foreach (PropertyInfo info in InterfaceProperties[type])
                {
                    CreateProperty(type, info);
                }
            }            
        }

        /// <summary>
        /// Creates a property.
        /// </summary>
        /// <param name="type">The type that declares the property.</param>
        /// <param name="property">The property.</param>
        private void CreateProperty(Type type, PropertyInfo property)
        {
            var code = new CodeMemberProperty();

            code.Name = string.Format("{0}.{1}", type.FullName, property.Name);
            code.Type = new CodeTypeReference(property.PropertyType);
            code.Attributes = MemberAttributes.Final;

            bool hasSet = property.CanWrite;
            code.HasGet = hasSet;

            bool hasGet = property.CanRead;
            code.HasSet = hasGet;

            if (hasSet)
            {
                var s = string.Format("base.SetPropertyValue(typeof({0}), \"{1}\", typeof({2}), value)",
                                      type.GetFullName(),
                                      property.Name,
                                      property.PropertyType.GetFullName());

                code.SetStatements.Add(new CodeSnippetExpression(s));
            }

            if (hasGet)
            {
                var s = string.Format("return ({0})base.GetPropertyValue(typeof({1}), \"{2}\", typeof({3}))",
                                      property.PropertyType.GetFullName(),
                                      type.GetFullName(),
                                      property.Name,
                                      property.PropertyType.GetFullName());

                code.GetStatements.Add(new CodeSnippetExpression(s));
            }

            // Resolve custom attributes.
            code.CustomAttributes.AddRange(CreateCustomAttributes(property));

            // Resolve referenced assemblies.
            ReferencedAssemblies.AddFromType(type);
            ReferencedAssemblies.AddFromType(property.PropertyType);

            // Add a property to the class.
            CompilationUnit.Namespaces[0].Types[0].Members.Add(code);
        }

        #endregion

        #region Events

        /// <summary>
        /// Creates events.
        /// </summary>
        private void CreateEvents()
        {
            foreach (Type type in InterfaceTypes)
            {
                foreach (EventInfo evt in InterfaceEvents[type])
                {
                    CreateEvent(type, evt);
                }
            }            
        }

        /// <summary>
        /// Creates an event.
        /// </summary>
        /// <param name="type">The type that declares the event.</param>
        /// <param name="evt">The event.</param>
        private void CreateEvent(Type type, EventInfo evt)
        {
            // Do not generate members for INotifyPropertyChanged and ICloneable interfaces, 
            // because ModelBase implements them!
            if (type == typeof(INotifyPropertyChanged))
            {
                return;
            }
            if (type == typeof(ICloneable))
            {
                return;
            }

            var code = new CodeMemberEvent();

            code.Name = string.Format("{0}.{1}", type.FullName, evt.Name);
            code.Type = new CodeTypeReference(evt.EventHandlerType);
            code.Attributes = MemberAttributes.Final;

            // Resolve custom attributes.
            code.CustomAttributes.AddRange(CreateCustomAttributes(evt));

            // Resolve referenced assemblies.
            ReferencedAssemblies.AddFromType(type);
            ReferencedAssemblies.AddFromType(evt.EventHandlerType);

            // Add an event to the class.
            CompilationUnit.Namespaces[0].Types[0].Members.Add(code);            
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creates methods.
        /// </summary>
        private void CreateMethods()
        {
            foreach (Type type in InterfaceTypes)
            {
                foreach (MethodInfo method in InterfaceMethods[type])
                {
                    CreateMethod(type, method);
                }
            }            
        }

        /// <summary>
        /// Creates a method.
        /// </summary>
        /// <param name="type">The type that declares the method.</param>
        /// <param name="method">The method.</param>
        private void CreateMethod(Type type, MethodInfo method)
        {
            var code = new CodeMemberMethod();

            code.Name = string.Format("{0}.{1}", type.GetFullName(), method.Name);
            code.ReturnType = new CodeTypeReference(method.ReturnType);
            code.Attributes = MemberAttributes.Final;

            ParameterInfo[] parameters = method.GetParameters();
            MethodParameterNameProvider nameProvider = new MethodParameterNameProvider(parameters);

            // var x = new MethodParameters();
            string s;
            string mp = nameProvider.GetNewName();
            s = string.Format("var {0} = new OperationPlugins.Models.MethodParameters()", mp);
            code.Statements.Add(new CodeSnippetExpression(s));


            foreach (ParameterInfo parameter in parameters)
            {
                // Resolve the parameter type and direction.
                Type parameterType;
                FieldDirection parameterDirection;
                if (parameter.IsOut)
                {
                    parameterType = parameter.ParameterType.GetElementType();
                    parameterDirection = FieldDirection.Out;
                }
                else if (parameter.ParameterType.IsByRef)
                {
                    parameterType = parameter.ParameterType.GetElementType();
                    parameterDirection = FieldDirection.Ref;
                }
                else
                {
                    parameterType = parameter.ParameterType;
                    parameterDirection = FieldDirection.In;
                }

                var p = new CodeParameterDeclarationExpression(parameterType, parameter.Name);

                p.Direction = parameterDirection;

                // Handle optional keyword.
                if (parameter.IsOptional)
                {
                    p.CustomAttributes.Add(new CodeAttributeDeclaration(
                                               new CodeTypeReference(typeof(OptionalAttribute))));

                    p.CustomAttributes.Add(new CodeAttributeDeclaration(
                                               new CodeTypeReference(typeof(DefaultParameterValueAttribute)),
                                                   new CodeAttributeArgument(
                                                       new CodePrimitiveExpression(parameter.DefaultValue))));
                }

                // Handle params keyword.
                if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute)))
                {
                    p.CustomAttributes.Add(new CodeAttributeDeclaration(
                                               new CodeTypeReference(typeof(ParamArrayAttribute))));
                }

                code.Parameters.Add(p);

                // Initialize out parameters before accessing them.
                if (parameter.IsOut)
                {
                    s = string.Format("{0} = default({1})", parameter.Name, parameterType.GetFullName());
                    code.Statements.Add(new CodeSnippetExpression(s));
                }

                s = string.Format("{0}[\"{1}\"] = {2}", mp, parameter.Name, parameter.Name);
                code.Statements.Add(new CodeSnippetExpression(s));
            }

            // Invoke the NotifyMethodInvoked method from the base class!
            string ret = nameProvider.GetNewName();
            s = string.Format("var {0} = base.NotifyMethodInvoked(typeof({1}), \"{2}\", {3})", ret, type.GetFullName(), method, mp);
            code.Statements.Add(new CodeSnippetExpression(s));

            // Collect out and byref parameter values.
            foreach (ParameterInfo parameter in parameters)
            {
                if (parameter.IsOut || parameter.ParameterType.IsByRef)
                {
                    Type parameterType = parameter.ParameterType.GetElementType();

                    s = string.Format("{0} = ({1}){2}[\"{3}\"]", parameter.Name, parameterType.GetFullName(), mp, parameter.Name);
                    code.Statements.Add(new CodeSnippetExpression(s));
                }
            }

            // Handle the method return value.
            if (method.ReturnType.FullName != "System.Void")
            {
                s = string.Format("return ({0}){1}.Value", method.ReturnType.GetFullName(), ret);
                code.Statements.Add(new CodeSnippetExpression(s));
            }

            // Resolve custom attributes.
            code.CustomAttributes.AddRange(CreateCustomAttributes(method));

            // Resolve referenced assemblies.
            ReferencedAssemblies.AddFromType(type);
            ReferencedAssemblies.AddFromType(method.ReturnType);
            foreach (ParameterInfo parameter in parameters)
            {
                ReferencedAssemblies.AddFromType(parameter.ParameterType);
            }

            // Add the method to the class.
            CompilationUnit.Namespaces[0].Types[0].Members.Add(code);            
        }

        private sealed class MethodParameterNameProvider
        {
            private readonly ParameterInfo[] _parameters;
            private int _counter;

            public MethodParameterNameProvider(ParameterInfo[] parameters)
            {
                _parameters = parameters;
            }

            public string GetNewName()
            {
                while (true)
                {
                    _counter++;
                    string name = string.Format("p{0}", _counter);

                    bool reserved = false;
                    foreach (ParameterInfo parameter in _parameters)
                    {
                        if (parameter.Name == name)
                        {
                            reserved = true;
                            break;
                        }
                    }

                    if (reserved == false)
                    {
                        return name;
                    }
                }
            }
        }

        #endregion

        #region Attributes

        /// <summary>
        /// Creates custom attributes for the specified member.
        /// </summary>
        /// <param name="member">The member.</param>
        /// <returns>Custom attributes.</returns>
        private CodeAttributeDeclaration[] CreateCustomAttributes(MemberInfo member)
        {
            IEnumerable<CustomAttributeData> items = member.GetCustomAttributesData();

            var declarations = new List<CodeAttributeDeclaration>();

            foreach (CustomAttributeData item in items)
            {
                var attribute = new CodeAttributeDeclaration(new CodeTypeReference(item.AttributeType));

                foreach (CustomAttributeTypedArgument arg in item.ConstructorArguments)
                {
                    var value = ParseArgumentValue(arg);
                    var exp = new CodeSnippetExpression(value);
                    var ca = new CodeAttributeArgument(exp);
                    attribute.Arguments.Add(ca);
                }

                if (item.NamedArguments != null)
                {
                    foreach (CustomAttributeNamedArgument arg in item.NamedArguments)
                    {
                        var value = ParseArgumentValue(arg);
                        var exp = new CodeSnippetExpression(value);
                        var ca = new CodeAttributeArgument(exp);
                        attribute.Arguments.Add(ca);
                    }
                }

                declarations.Add(attribute);
            }

            return declarations.ToArray();
        }

        private static string ParseArgumentValue(CustomAttributeTypedArgument arg)
        {
            foreach (ICustomAttributeTypedArgumentValueParser parser in Parsers)
            {
                string value;
                if (parser.TryParse(arg, out value))
                {
                    return value;
                }
            }

            return arg.ToString();
        }

        private static string ParseArgumentValue(CustomAttributeNamedArgument arg)
        {
            foreach (ICustomAttributeTypedArgumentValueParser parser in Parsers)
            {
                string value;
                if (parser.TryParse(arg.TypedValue, out value))
                {
                    return string.Format("{0} = {1}", arg.MemberName, value);
                }
            }

            return arg.ToString();
        }

        #endregion

        #region Interface Members

        /// <summary>
        /// Resolves interface members.
        /// </summary>
        private void ResolveInterfaceMembers()
        {
            InterfaceTypes = new List<Type>();
            InterfaceProperties = new Dictionary<Type, List<PropertyInfo>>();
            InterfaceMethods = new Dictionary<Type, List<MethodInfo>>();
            InterfaceEvents = new Dictionary<Type, List<EventInfo>>();
            
            InterfaceTypes.Add(Interface);
            InterfaceTypes.AddRange(GetInterfaces(Interface));

            foreach (Type type in InterfaceTypes)
            {
                var properties = type.GetProperties();
                var propertyList = new List<PropertyInfo>(properties);
                InterfaceProperties.Add(type, propertyList);

                var methods = type.GetMethods().Where(x => x.IsSpecialName == false);
                var methodList = new List<MethodInfo>(methods);
                InterfaceMethods.Add(type, methodList);

                var events = type.GetEvents();
                var eventList = new List<EventInfo>(events);
                InterfaceEvents.Add(type, eventList);
            }
        }

        private static IEnumerable<Type> GetInterfaces(Type interfaceType)
        {
            var l = new List<Type>();
            foreach (Type type in interfaceType.GetInterfaces())
            {
                l.Add(type);
                l.AddRange(GetInterfaces(type));
            }
            return l;
        }

        #endregion

        /// <summary>
        /// Initialize. 
        /// </summary>
        [Export(typeof(IInitializable))]
        private sealed class Initializer : IInitializable
        {
            void IInitializable.Initialize(InitializationContext context)
            {
                CodeGenerators = context.CompositionContainer.GetPlugins<IExternalCodeGenerator>();
                Parsers = context.CompositionContainer.GetPlugins<ICustomAttributeTypedArgumentValueParser>();
                ProxyAssemblyNotificationListeners = context.CompositionContainer.GetPlugins<IProxyAssemblyNotificationListener>();
            }
        }

    }
}
